-
Notifications
You must be signed in to change notification settings - Fork 29.6k
fix(84750): await metadata promises to ensure proper head placement #84976
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
fix(84750): await metadata promises to ensure proper head placement #84976
Conversation
Fixes vercel#84750 Made Metadata() functions async and await the metadata promise before rendering. This ensures metadata is resolved during initial SSR rather than being deferred via Suspense/Flight data, allowing React's server-side hoisting to correctly place all meta tags in <head> instead of <body>. Root cause: When metadata goes through Suspense boundaries, it's sent via RSC Flight data after the initial HTML stream has already sent <head>. React cannot hoist backwards in a stream, so tags end up in <body>. This fix: - Forces metadata resolution before streaming begins - Metadata becomes part of initial SSR HTML where hoisting works - Removes need for @ts-expect-error comments (proper async/await pattern) Trade-off: Response blocks on metadata resolution (~milliseconds), but this is acceptable since metadata is invisible and correctness (SEO, social sharing) is more critical than micro-optimizations.
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
a13eea7
to
1e900de
Compare
Updates test expectations to match the fix where metadata now renders in <head> instead of <body>. Changes: - metadata-streaming.test.ts: Update all tests to expect metadata in head - metadata-streaming-static-generation.test.ts: Update dev and dynamic tests These tests were previously checking that metadata was incorrectly placed in <body>, which was the bug. Now they correctly verify that all metadata tags are in <head> for proper SEO and social sharing.
ok updated tests:
|
…etadata durting the initial SSR phase ### Changes **Core Fix:** - `packages/next/src/lib/metadata/metadata.tsx`: - Removed conditional logic based on `serveStreamingMetadata` - Always use async components that await metadata resolution - Removed `Suspense` wrapper from `MetadataOutlet` - Added explanatory comment about why metadata must be awaited **Test Updates:** Updated tests to expect the new correct behavior (metadata in `<head>`): - `test/e2e/app-dir/metadata-streaming/metadata-streaming-customized-rule.test.ts` - `test/e2e/app-dir/metadata-streaming-parallel-routes/metadata-streaming-parallel-routes.test.ts` - `test/e2e/app-dir/metadata-static-generation/metadata-static-generation.test.ts` - `test/e2e/app-dir/metadata-icons/metadata-icons.test.ts` ### Trade-offs - Response may be delayed while metadata resolves (typically <1ms for static metadata) - Metadata is invisible, so no perceived user impact - Correctness (SEO, social, HTML validity) > microsecond optimization ### Breaking Changes - The `htmlLimitedBots` configuration now has reduced effect since all requests get blocking metadata - Streaming metadata feature is effectively disabled for initial page loads ### References - Original issue: vercel#84750 - Root cause analysis in ai-docs/FINAL_ROOT_CAUSE_ANALYSIS.md
getDynamicParamFromSegment, | ||
errorType, | ||
workStore, | ||
serveStreamingMetadata, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The serveStreamingMetadata
parameter is declared in the type signature but not destructured in the function parameters, causing an incomplete refactoring. This should be removed from the type signature since it's no longer used.
View Details
📝 Patch Details
diff --git a/packages/next/src/lib/metadata/metadata.tsx b/packages/next/src/lib/metadata/metadata.tsx
index 86fbf614fb..1b2bef98a5 100644
--- a/packages/next/src/lib/metadata/metadata.tsx
+++ b/packages/next/src/lib/metadata/metadata.tsx
@@ -65,7 +65,6 @@ export function createMetadataComponents({
getDynamicParamFromSegment: GetDynamicParamFromSegment
errorType?: MetadataErrorType | 'redirect'
workStore: WorkStore
- serveStreamingMetadata: boolean
}): {
Viewport: React.ComponentType
Metadata: React.ComponentType
diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx
index 3c9423424e..3e71fc8f44 100644
--- a/packages/next/src/server/app-render/app-render.tsx
+++ b/packages/next/src/server/app-render/app-render.tsx
@@ -495,7 +495,7 @@ async function generateDynamicRSCPayload(
metadataContext: createMetadataContext(ctx.renderOpts),
getDynamicParamFromSegment,
workStore,
- serveStreamingMetadata,
+
})
flightData = (
@@ -1254,7 +1254,6 @@ async function getRSCPayload(
metadataContext: createMetadataContext(ctx.renderOpts),
getDynamicParamFromSegment,
workStore,
- serveStreamingMetadata,
})
const preloadCallbacks: PreloadCallbacks = []
@@ -1375,7 +1374,7 @@ async function getErrorRSCPayload(
errorType,
getDynamicParamFromSegment,
workStore,
- serveStreamingMetadata: serveStreamingMetadata,
+
})
const initialHead = createElement(
Analysis
Incomplete refactoring: unused serveStreamingMetadata parameter in createMetadataComponents
What fails: The createMetadataComponents()
function has serveStreamingMetadata: boolean
in its type signature but never destructures or uses it in the function body. This creates an inconsistency where callers must provide the parameter even though it's ignored at runtime.
How to reproduce: Look at the function definition in packages/next/src/lib/metadata/metadata.tsx
(lines 53-68):
- Lines 53-59 destructure parameters:
tree
,pathname
,parsedQuery
,metadataContext
,getDynamicParamFromSegment
,errorType
,workStore
- Line 68 declares in type signature:
serveStreamingMetadata: boolean
(NOT destructured) - Function body (lines 72+) never references
serveStreamingMetadata
- Callers in
app-render.tsx
(lines 498, 1257, 1378) provideserveStreamingMetadata
but it's silently ignored
Result: Parameter is silently ignored at runtime; incomplete refactoring where the parameter should have been removed from the type signature.
Expected: The type signature should only include parameters that are actually used. The serveStreamingMetadata
parameter has been removed from the destructuring but was left in the type signature during a refactoring.
Fix applied:
- Removed
serveStreamingMetadata: boolean
from the type signature inpackages/next/src/lib/metadata/metadata.tsx
line 68 - Removed
serveStreamingMetadata
parameter from all three call sites inpackages/next/src/server/app-render/app-render.tsx
(lines 498, 1257, 1378)
Fixes #84750
Made Metadata() functions async and await the metadata promise before rendering. This ensures metadata is resolved during initial SSR rather than being deferred via Suspense/Flight data, allowing React's server-side hoisting to correctly place all meta tags in instead of .
Root cause: When metadata goes through Suspense boundaries, it's sent via RSC Flight data after the initial HTML stream has already sent . React cannot hoist backwards in a stream, so tags end up in .
This fix:
Trade-off: Response blocks on metadata resolution (~milliseconds), but this is acceptable since metadata is invisible and correctness (SEO, social sharing) is more critical than micro-optimizations.
(working on updating the tests....)